"""
数据提取-单接口测试
"""
import json
import re
from json.decoder import JSONDecodeError

import allure
import jsonpath

from config.setting import FILE_PATH
from utils.assertion.assertions import Assertions
from utils.debugtalk import DebugTalk
from utils.logging_tool.recordlog import logs
from utils.mysql_tool.connect_mysql import ConnectMysql
from utils.read_files_tools.read_extract_yaml import ReadYamlData
from utils.requests_tools.send_request import SendRequest


class RequestBase:

    def __init__(self):
        self.run = SendRequest()
        self.read = ReadYamlData()
        self.asserts = Assertions()

    def replace_load(self, data):
        """
        解析并替换 yaml数据中的变量引用，如 ${get_extract_data(Cookie)}
        :param data: 解析的 yaml 数据
        :return: 返回的是 dict 类型
        """
        str_data = data
        # 判断yaml 数据类型，只有字符串才能使用
        if not isinstance(data, str):
            # 如果不是则转成字符串
            str_data = json.dumps(data, ensure_ascii=False)

        # 循环判断字符串 出现 '${' 的次数
        for i in range(str_data.count('${')):

            if '${' in str_data and '}' in str_data:
                # 获取索引开始和结束位置
                start_index = str_data.index('$')
                end_index = str_data.index('}', start_index)

                # 切片规则：开始的位置包含，结束的位置不包含，所以结束位置 +1
                ref_all_params = str_data[start_index:end_index + 1]

                # 取出yaml文件的函数名
                func_name = ref_all_params[2:ref_all_params.index("(")]
                # 取出函数里面的参数
                func_params = ref_all_params[ref_all_params.index("(") + 1:ref_all_params.index(")")]

                # 传入替换的参数获取对应的值,类的反射----getattr,setattr,del....
                extract_data = getattr(DebugTalk(), func_name)(*func_params.split(',') if func_params else "")

                # 判断 extract_data 是否为列表类型
                if extract_data and isinstance(extract_data, list):
                    extract_data = ','.join(e for e in extract_data)
                # 替换数据，字符串类型
                str_data = str_data.replace(ref_all_params, str(extract_data))
                # print('通过解析后替换的数据：', str_data)

        # 还原数据，将字符串str 类型 转换 为字典 dict类型
        if data and isinstance(data, dict):
            data = json.loads(str_data)
        else:
            data = str_data

        return data

    def specification_yaml(self, base_info, test_case):
        """
        规范yaml接口信息，执行接口、提取
        接口请求处理基本方法
        :param base_info: yaml文件里面的baseInfo
        :param test_case: yaml文件里面的testCase
        :return:
        """
        cookies = None
        try:
            # ----------------------------------------处理baseInfo里面的数据----------------------------------------
            # 模块名称
            feature = base_info['feature']
            allure.attach(feature, f'模块名称：{feature}')

            # 接口名称
            api_name = base_info['api_name']
            allure.attach(api_name, f'接口名称：{api_name}')

            # 接口地址
            url = DebugTalk().get_baseurl() + base_info['url']
            allure.attach(url, f'接口地址：{url}')

            # 请求方法
            method = base_info['method']
            allure.attach(method, f'请求方法：{method}')

            # 请求头
            headers = base_info.get('headers', None)  # 如果键不存在则返回该默认值 None
            if headers is not None:
                # yaml 文件中是 ${get_headers(data)} 提取后是字符串格式， eval 字符串转成字典格式；
                headers = eval(self.replace_load(base_info['headers'])) if isinstance(headers, str) else headers

            allure.attach(str(headers), f'请求头：{headers}', allure.attachment_type.JSON)

            # cookie-【使用session用不到】
            cookies = base_info.get('cookies', None)
            if cookies is not None:
                cookies = eval(self.replace_load(base_info['cookies'])) if isinstance(cookies, str) else cookies

            # ----------------------------------------处理testCase里面的数据----------------------------------------

            # 测试用例名称
            case_name = test_case.pop('case_name', None)  # 使用 pop() 方法删除指定键值对并返回其值
            allure.attach(case_name, f'测试用例名称：{case_name}')

            # 用例等级
            rank = test_case.pop('rank', None)
            allure.attach(rank, f'严重等级：{rank}')

            # 下载文件类型
            download_type = test_case.pop("download_type", None)

            # 用例前置条件
            if 'setup' in test_case:
                # 获取前置方式
                if 'type' or 'database' or 'sqls' != None:
                    test_case['setup'].pop('type', None)
                    test_case['setup'].pop('database', None)
                    test_case['setup'].pop('sqls', None)
            test_case.pop('setup', None)

            # 用例后置条件
            if 'teardown' in test_case:
                # 获取后置方式
                if 'type' or 'database' or 'sqls' != None:
                    test_case['teardown'].pop('type', None)
                    test_case['teardown'].pop('database', None)
                    test_case['teardown'].pop('sqls', None)
            test_case.pop('teardown', None)

            # 断言
            val = self.replace_load(test_case.get('validation'))  # replace_load 断言结果解析替换
            test_case['validation'] = val
            validation = eval(test_case.pop('validation', None))
            allure_validation = str([str(list(i.values())) for i in validation])
            allure.attach(allure_validation, "预期结果", allure.attachment_type.JSON)

            # 参数提取
            extract = test_case.pop('extract', None)
            extract_list = test_case.pop('extract_list', None)

            # 接口的请求参数，使用pop方式 将 case_name、validation等都删除 剩下的处理接口请求参数
            params_type = ['params', 'data', 'json']
            for key, value in test_case.items():
                if key in params_type:
                    # 里面的参数也可以用提取的参数，比如 passwd:${get_extract_data(password)}
                    result = self.replace_load(value)
                    test_case[key] = result

            # 文件上传接口
            files, file = test_case.pop('files', None), None
            if files is not None:
                print(files)
                for fk, fv in files.items():
                    allure.attach(json.dumps(files), '导入文件')
                    files = {fk: open(fv, mode='rb')}

            res = self.run.run_main(feature=feature,
                                    name=api_name,
                                    case_name=case_name,
                                    url=url,
                                    headers=headers,
                                    method=method,
                                    files=files,
                                    cookies=cookies,
                                    **test_case)

            # 处理下载文件类型的接口请求
            if download_type is not None:
                with open(f"{FILE_PATH['DOWNLOAD']}{case_name}{download_type}", "wb") as f:
                    f.write(res.content)

            try:
                status_code = res.status_code
                logs.info(f'接口响应状态码：{status_code}')
                allure.attach(str(status_code), f"接口响应状态码:{str(status_code)}")

                # 判断接口响应信息是否为json格式
                is_json_pattern = r'^\s*[\{\[].*[\}\]]\s*$'
                if re.match(is_json_pattern, res.text) is None:

                    logs.info("接口响应信息非json格式，不支持接口信息提取，断言只支持状态码断言")

                    # 处理状态码断言
                    self.asserts.assert_result(validation, None, status_code)

                else:
                    # 把json格式转换成字典字典
                    res_json = json.loads(res.text)

                    # 根据接口信息进行提取
                    if extract is not None:
                        self.extract_data(extract, res.text)
                    if extract_list is not None:
                        self.extract_data_list(extract_list, res.text)

                    # 处理断言
                    self.asserts.assert_result(validation, res_json, status_code)

                    allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.JSON)

            except JSONDecodeError as js:
                logs.error('系统异常或接口未请求！')
                raise js
            except Exception as e:
                logs.error(e)
                raise e

        except Exception as e:
            raise e

    @classmethod
    def allure_attach_response(cls, response):
        if isinstance(response, dict):
            allure_response = json.dumps(response, ensure_ascii=False, indent=4)
        else:
            allure_response = response
        return allure_response

    @allure.step("测试用例前置")
    def testcase_setup(self, testcases, index):
        """
        测试用例前置操作,从yaml 文件中获取前置方式
        :param testcases:获取到的所有用例
        :param index:
        :return:
        """
        if len(testcases) <= index:
            return
        i = 0
        for i in range(index, index + 1):
            testcase = testcases[i]
            casename = testcase[1].get('case_name')
            logs.info(f'【用例标题】{casename}')
            logs.info(f'【用例信息】{testcase}')
            try:
                if 'setup' in testcase[1]:  # 前置操作
                    # 获取前置方式
                    type = testcase[1]['setup'].get('type', None)
                    if type == 'db':  # 数据库操作
                        database = testcase[1]['setup'].get('database', None)
                        sqls = testcase[1]['setup'].get('sqls', None)
                        if sqls is None:
                            logs.info(f'sql语句为空，未进行前置操作')
                        else:  # sql语句不为空
                            ConnectMysql(database).execute_sqls(sqls)
                            allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                            allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                    elif type == 'api':  # 调用接口
                        logs.info('暂不支持api的配置方式，敬请期待~~')
                    elif type is None:
                        logs.info('前置方式为空,未进行前置操作')
                    else:
                        logs.error(f'不支持的前置方式:{type}，未进行前置操作')
                else:
                    logs.info('不需要前置操作')
            except Exception as e:
                logs.error(f'前置操作失败！err={e}')

    @allure.step("测试用例后置")
    def testcase_teardown(self, testcases, index):
        """
        测试用例后置操作, 从yaml 文件中获取后置方式
        :param type：填写 db 和 api
        :param testcases: 获取到的所有用例
        :param sqls: sql语句，支持多条
        :return:
        """
        if len(testcases) <= index:
            return
        i = 0
        for i in range(index, index + 1):
            testcase = testcases[i]
            try:
                if 'teardown' in testcase[1]:
                    type = testcase[1]['teardown'].get('type', None)
                    if type == 'db':  # 数据库操作
                        database = testcase[1]['teardown'].get('database', None)
                        sqls = testcase[1]['teardown'].get('sqls', None)
                        if sqls is not None:  # sql语句不为空
                            ConnectMysql(database).execute_sqls(sqls)
                            allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                            allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                    elif type == 'api':  # 调用接口
                        logs.info('暂不支持api的配置方式，敬请期待~~')
                    elif type is None:
                        logs.error('后置方式为空,后置操作失败！')
                    else:
                        logs.error(f'不支持的后置方式:{type}，后置操作失败！')
                else:
                    logs.info('不需要后置操作')
            except Exception as e:
                logs.error(f'后置操作失败！err={e}')

    @allure.step("提取数据：正则表达式/JsonPath")
    def extract_data(self, testcase_extarct, response):
        """
        提取接口的返回值，支持正则表达式和json提取器
        :param testcase_extarct: testcase文件yaml中的extract值
        :param response: 接口的实际返回值
        :return:
        """
        try:
            pattern_lst = ['(.*?)', '(.+?)', r'(\d)', r'(\d*)']
            for key, value in testcase_extarct.items():

                # 处理正则表达式提取
                for pat in pattern_lst:
                    if pat in value:
                        ext_lst = re.search(value, response)
                        if pat in [r'(\d+)', r'(\d*)']:
                            extract_data = {key: int(ext_lst.group(1))}
                            logs.info('正则提取到的参数：%s' % extract_data)
                        else:
                            extract_data = {key: ext_lst.group(1)}
                        self.read.write_yaml_data(extract_data)

                # 处理json提取参数
                if '$' in value:
                    ext_json = jsonpath.jsonpath(json.loads(response), value)[0]
                    if ext_json:
                        extarct_data = {key: ext_json}
                        logs.info('json提取到参数：%s' % extarct_data)
                    else:
                        extarct_data = {key: '未提取到数据，请检查接口返回值是否为空！'}
                    self.read.write_yaml_data(extarct_data)
        except Exception as e:
            logs.error(e)

    @allure.step("提取数据：正则表达式/JsonPath-结果为列表")
    def extract_data_list(self, testcase_extract_list, response):
        """
        提取多个参数，支持正则表达式和json提取，提取结果以列表形式返回
        :param testcase_extract_list: yaml文件中的 extract_list信息
        :param response: 接口的实际返回值,str类型
        :return:
        """
        try:
            for key, value in testcase_extract_list.items():
                if "(.+?)" in value or "(.*?)" in value:
                    ext_list = re.findall(value, response, re.S)
                    if ext_list:
                        extract_data = {key: ext_list}
                        logs.info('正则提取到的参数：%s' % extract_data)
                        self.read.write_yaml_data(extract_data)
                if "$" in value:
                    # 增加提取判断，有些返回结果为空提取不到，给一个默认值
                    ext_json = jsonpath.jsonpath(json.loads(response), value)
                    if ext_json:
                        extract_data = {key: ext_json}
                    else:
                        extract_data = {key: "未提取到数据，该接口返回结果可能为空"}
                    logs.info('json提取到参数：%s' % extract_data)
                    self.read.write_yaml_data(extract_data)
        except:
            logs.error('接口返回值提取异常，请检查yaml文件extract_list表达式是否正确！')
